<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PianoRoll Demo</title>
    <style>
        .piazi{
            width: 25px;
        }
    </style>
</head>
<body>
    拍号设置(分子/分母)<input class="piazi" type="number" min="1" value="4" onchange="changeBeatFenzi(this)">
    <input class="piazi" type="number" min="1" value="4" onchange="changeBeatFenmu(this)">
    <!-- 这里是考虑到移植安卓作曲软件里的问题 -->
    <!-- 乐器选择
    <select>
        <option value="piano">钢琴</option>
        <option value="guitar">吉他</option>
    </select> -->
    <div>吸附单位选择
        <select onchange="changeGrid(this.value)">
            <option value="1">1/1(节拍)</option>
            <option value="2">1/2</option>
            <option value="4">1/4</option>
            <option value="8">1/8</option>
            <option value="16">1/16</option>
            <option value="32">1/32</option>
            <option value="64">1/64</option>
            <option value="3">1/3（三连音）</option>
            <option value="6">1/6（三连音）</option>
            <option value="12">1/12（三连音）</option>
            <option value="24">1/24（三连音）</option>
            <option value="48">1/48（三连音）</option>
        </select>
       
        拉伸音符<input type="checkbox" onchange="changeStretch(this)">
        框选<input id="rect-select" type="checkbox" onchange="changeSelectMode(this)"/>
        <button onclick="copy()">复制</button>
        <button onclick="paste()">粘贴</button>
        <button onclick="delSelPattern()">清空</button>
        <button onclick="delSelNote()">删除选中</button>
    </div>
    <div id="pattern-list" style="width: 800px;overflow-y: auto;white-space: nowrap;">旋律片段</div>
    <canvas id="piano-roll" width="864" height="384"></canvas>
    <canvas id="vertical-scrollbar" width="40" height="384"></canvas>
    <script>
        var copyedPatternData 
        var paiziSettings = {"fenzi":4, "fenmu":4}
        //框选工具
        const RectSelect = document.getElementById('rect-select');
        // 框选模式
        var isRectSelectMode = false
        var snapDanwei = 4
        const PatternList = document.getElementById('pattern-list');//uiPatternList
        var selectedNoteIndex = -1;
        // 框选相关变量
        var selectedNoteIndices = []; // 多选索引数组
        // 绘制框选矩形
        var isRectSelecting = false;
        var rectSelectStartX = 0;
        var rectSelectStartY = 0;
        var rectSelectEndX = 0;
        var rectSelectEndY = 0;
        
        const all_note_width = 800
        const note_height = 32 // 重构为grid_size = {"width","height"}
        //一个音阶，0表示白键，1表示黑键
        const melody_scale = [0,1,0,1,0,0,1,0,1,0,1,0]
        const keyWidth = 64;
        var dragOffsetX = 0
        var dragOffsetY = 0
        var mouseDownX = 0
        var mouseDownY = 0
        // 要绘制的音符宽度
        var note_width = all_note_width
        var grid = 16
        // 每个小节的音符序列
        var seq = []
        var pattern = []
        var selPatternIndex = -1
        const HANDLE_WIDTH = 8 // 拉伸手柄的宽度
        var isNoteStretch = false
        var isMouseDown = false
        var isStretching = false
        var dragNoteIndex = -1; // 正在拖动或拉伸的音符索引（锚点）
        document.addEventListener('contextmenu',function(e)
        {
            e.preventDefault();
        })
        const canvas = document.getElementById('piano-roll');
        CanvasRenderingContext2D.prototype.drawLine = function(x1, y1, x2, y2) 
        {
            this.beginPath();
            this.moveTo(x1, y1);
            this.lineTo(x2, y2);
            this.stroke();
        };
        const Color = {
            "black": "#000000",
            "white": "#ffffff",
            "grey": "#808080",
            "green":"#008000"
        }
        const ctx = canvas.getContext('2d');
        const scrollbarCanvas = document.getElementById('vertical-scrollbar');
        const scrollbarCtx = scrollbarCanvas.getContext('2d');
        const scrollbarSliderHeight = 20
        var scrollbarUiData = {
            "y":0,
            "isMouseDown":false
        }
        const TOTAL_OCTAVES = 7; //C1-C8
        const NOTES_PER_OCTAVE = 12;
        const TOTAL_NOTES = TOTAL_OCTAVES * NOTES_PER_OCTAVE;
        var scrollOffset = 0;
        const maxScrollOffset = (TOTAL_NOTES * note_height) - canvas.height;
        
        var full_keyboard_map = [];
        for(var octave=0;octave<TOTAL_OCTAVES;octave++)
        {
            full_keyboard_map.push(...melody_scale)
        }
        function initUI()
        {
            for(var i = 0; i < 512; i++)
            {
                addPattern(i)
            }
            selPattern(PatternList.children[0])
            redraw();
        }
        initUI()
        function snap(value,grid_space)
        {
            return Math.floor(value / grid_space) * grid_space;
        }
        function redrawKey()
        {
            for(var i = 0; i < TOTAL_NOTES; i++)
            {
                var canvasY = musicToCanvasY(i*note_height) - scrollOffset;
                if(canvasY + note_height < 0 || canvasY > canvas.height) continue;                
                if(full_keyboard_map[i] === 0)
                {
                    ctx.fillStyle = Color.white;
                }
                else{
                    ctx.fillStyle = Color.black;
                }
                ctx.fillRect(0, canvasY, keyWidth, note_height);
                ctx.strokeRect(0, canvasY, keyWidth, note_height);
                // 检查是否为 C 音（每个八度的第一个音）
                if(i % 12 === 0) {
                    const octaveNumber = Math.floor(i / 12) + 1; // C1, C2, ..., C8
                    const noteName = "C" + octaveNumber;
                    ctx.fillStyle = Color.black;
                    ctx.font = "14px Arial";
                    const textY = canvasY + note_height / 2 + 5;
                    ctx.fillText(noteName, keyWidth - ctx.measureText(noteName).width - 5, textY);
                }
            }
        }
        function redrawGrid()
        {
            //这里应该从下往上绘制
            for(var i = 0; i < TOTAL_NOTES; i++)
            {
                var canvasY = musicToCanvasY(i*note_height) - scrollOffset;
                // 只绘制可见区域内的网格
                if(canvasY + note_height < 0 || canvasY > canvas.height) continue;
                if(full_keyboard_map[i] === 0)
                {
                    ctx.fillStyle = Color.white;
                }
                else{
                    ctx.fillStyle = Color.grey;
                }
                ctx.fillRect(keyWidth, canvasY, canvas.width-keyWidth, note_height)
            }
            ctx.fillStyle = "#000000"
            ctx.lineWidth = 1
                    // 绘制垂直网格线
            const gridSpacing = all_note_width / grid;
            for (let i = 0; i <= grid; i++) {
                const x = i * gridSpacing + keyWidth;
                ctx.drawLine(x, 0, x, canvas.height);
            }
                // 绘制水平线（钢琴键）
            for (let i = 0; i <= TOTAL_NOTES; i++) {
                const y = musicToCanvasY(i * note_height) - scrollOffset;
                // 只绘制可见区域内的线
                if(y >= 0 && y <= canvas.height) {
                    ctx.drawLine(keyWidth, y, canvas.width, y);
                }
            }
        }
        function redrawScrollbar()
        {
            scrollbarCtx.clearRect(0, 0, scrollbarCanvas.width, scrollbarCanvas.height);
            scrollbarCtx.fillStyle = Color.black;
            scrollbarCtx.fillRect(0, 0, scrollbarCanvas.width, scrollbarCanvas.height);
              // 计算滚动条拇指的位置
            const scrollbarRatio = scrollOffset / maxScrollOffset;
            scrollbarUiData.y = scrollbarRatio * (scrollbarCanvas.height - scrollbarSliderHeight);
            scrollbarCtx.fillStyle = Color.green;
            scrollbarCtx.fillRect(0, scrollbarUiData.y, scrollbarCanvas.width, scrollbarSliderHeight);   
        }
        function redraw()
        {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            redrawGrid()
            redrawKey()
            redrawScrollbar()
            for(var i = 0; i < seq.length; i++)
            {
                var noteX = seq[i].x + keyWidth;
                var noteY = musicToCanvasY(seq[i].y) - scrollOffset;
                 // 只绘制可见区域内的音符
                if(noteY + seq[i].height < 0 || noteY > canvas.height) continue;

                const isSelected = selectedNoteIndex == i || selectedNoteIndices.includes(i); 

                // 检查是否被选中（单选或多选）
                if(isSelected)
                {
                    ctx.fillStyle = "#00ff00"
                }
                else{
                    ctx.fillStyle = "#ff0000"
                }
                ctx.fillRect(noteX, noteY, seq[i].width, seq[i].height)
                
                // 绘制拉伸手柄 (如果启用拉伸且音符被选中)
                if(isNoteStretch && isSelected)
                {
                    ctx.fillStyle = "#008800"
                    ctx.fillRect(noteX + seq[i].width - HANDLE_WIDTH/2, 
                         noteY + 2, 
                         HANDLE_WIDTH, 
                         seq[i].height - 4);
                }
            }
            // 绘制框选矩形
            if (isRectSelecting && isRectSelectMode) {
                ctx.save()
                ctx.strokeStyle = "#0000ff";
                ctx.lineWidth = 2;
                ctx.setLineDash([5, 5]); // 虚线样式
                
                const x = Math.min(rectSelectStartX, rectSelectEndX);
                const y = Math.min(rectSelectStartY, rectSelectEndY);
                const width = Math.abs(rectSelectEndX - rectSelectStartX);
                const height = Math.abs(rectSelectEndY - rectSelectStartY);
                
                ctx.strokeRect(x, y, width, height);
                ctx.setLineDash([]); // 重置虚线样式
                ctx.restore()
            }
        }
        function hitTest(x,y)
        {
            var gridX = x - keyWidth
            if(x< keyWidth)
            {
                return { hit: false, index: -1, note: null };
            }
            // 从后往前检查，因为后绘制的音符在视觉上在顶层
            for (let i = seq.length - 1; i >= 0; i--) 
            {
                const note = seq[i];
                var noteScreenX = note.x + keyWidth
                var noteScreenY = musicToCanvasY(note.y) - scrollOffset;
                // 检查点是否在矩形区域内
                if (x >= noteScreenX && x <= noteScreenX + note.width && y >= noteScreenY && y <= noteScreenY + note.height) 
                {
                    return { hit: true, index: i, note: note };
                }
            }
            return { hit: false, index: -1, note: null };
        }
        // 检查是否点击在音符拉伸手柄上，返回被点击的音符索引，否则返回 -1
        function hitTestHandle(x,y)
        {
            if(!isNoteStretch) return -1;

            // 确定要检查的音符列表
            let indicesToCheck = [];
            if (selectedNoteIndex >= 0 && selectedNoteIndices.length === 0) {
                // 单选模式
                indicesToCheck.push(selectedNoteIndex);
            } else if (selectedNoteIndices.length > 0) {
                // 多选模式
                indicesToCheck = selectedNoteIndices;
            } else {
                return -1; // 没有音符被选中
            }
            
             // 从后往前检查，优先检测视觉上在顶层的音符
             for (let i = seq.length - 1; i >= 0; i--) {
                if (indicesToCheck.includes(i)) {
                    const note = seq[i];
                    var noteScreenX = note.x + keyWidth;
                    var noteScreenY = musicToCanvasY(note.y) - scrollOffset;
                    // 检查是否点击在拉伸手柄上
                    if (x >= noteScreenX + note.width - HANDLE_WIDTH/2 && 
                        x <= noteScreenX + note.width + HANDLE_WIDTH/2 &&
                        y >= noteScreenY && 
                        y <= noteScreenY + note.height) 
                    {
                        // 返回被点击的音符索引作为锚点
                        return i; 
                    }
                }
            }
            return -1;
        }
        // 检查音符是否在框选区域内
        function isNoteInRectSelection(noteIndex) {
            const note = seq[noteIndex];
            const noteX = note.x + keyWidth;
            const noteY = musicToCanvasY(note.y) - scrollOffset;
            const noteRight = noteX + note.width;
            const noteBottom = noteY + note.height;
            
            const rectLeft = Math.min(rectSelectStartX, rectSelectEndX);
            const rectTop = Math.min(rectSelectStartY, rectSelectEndY);
            const rectRight = Math.max(rectSelectStartX, rectSelectEndX);
            const rectBottom = Math.max(rectSelectStartY, rectSelectEndY);
            
            // 检查音符是否与框选区域相交
            return noteRight > rectLeft && noteX < rectRight && noteBottom > rectTop && noteY < rectBottom;
        }
        function delSelNote()
        {
            // ... (保持不变)
            if(selectedNoteIndex >= 0)
            {
                seq.splice(selectedNoteIndex, 1);
                selectedNoteIndex = -1
                dragNoteIndex = -1; 
                console.log(seq)
                pattern[selPatternIndex].seq = [...seq]
            }
            // 【新增】删除多选音符
            if (selectedNoteIndices.length > 0) {
                // 从后往前删除，避免索引变化问题
                selectedNoteIndices.sort((a, b) => b - a).forEach(index => {
                    seq.splice(index, 1);
                });
                selectedNoteIndices = [];
                dragNoteIndex = -1;
                pattern[selPatternIndex].seq = [...seq];
            }

            redraw()
        }
        canvas.addEventListener('mousedown', function(e) 
        {
            isMouseDown = true
            isStretching = false
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            mouseDownX = x
            mouseDownY = y
            var gridX = x -keyWidth
            
            // 检查是否点击在拉伸手柄上
            const handleIndex = hitTestHandle(x,y); 
            if(handleIndex >= 0)
            {
                isStretching = true
                // 使用被点击的音符作为拉伸锚点
                dragNoteIndex = handleIndex; 
                // dragOffsetX 用于计算新宽度
                dragOffsetX = gridX - (seq[dragNoteIndex].x + seq[dragNoteIndex].width);
                return
            }

            // 如果启用框选模式且点击在音符区域
            if (isRectSelectMode && x >= keyWidth) {
                isRectSelecting = true;
                rectSelectStartX = x;
                rectSelectStartY = y;
                rectSelectEndX = x;
                rectSelectEndY = y;
                
                // 如果是多选模式（按住Ctrl键）
                if (!e.ctrlKey) {
                    // 清除之前的选择
                    selectedNoteIndex = -1;
                    selectedNoteIndices = [];
                    dragNoteIndex = -1; 
                }
                redraw();
                return;
            }
            
            // 【修复】重写音符命中逻辑
            var hitResult = hitTest(x, y);
            if(hitResult && hitResult.hit)
            {
                const clickedNoteIndex = hitResult.index;
                const clickedNoteAlreadySelected = selectedNoteIndices.includes(clickedNoteIndex);
                dragNoteIndex = -1; // 默认不拖动

                if (e.ctrlKey) {
                    // Ctrl-click: 切换选择
                    if (clickedNoteAlreadySelected) {
                        selectedNoteIndices = selectedNoteIndices.filter(index => index !== clickedNoteIndex);
                    } else {
                        selectedNoteIndices.push(clickedNoteIndex);
                    }
                    selectedNoteIndex = -1; // 确保关闭单选
                } else if (clickedNoteAlreadySelected) {
                    // 点击一个已经被多选的音符: 准备拖动所有
                    selectedNoteIndex = -1; // 关闭单选
                    dragNoteIndex = clickedNoteIndex; // 记录我们正在拖动这个音符
                } else {
                    // 标准的单击一个新音符
                    selectedNoteIndex = clickedNoteIndex;
                    dragNoteIndex = clickedNoteIndex; // 记录我们正在拖动这个音符
                    selectedNoteIndices = []; // 清空多选
                }
                
                // 计算拖动偏移量 (如果准备拖动)
                if (dragNoteIndex >= 0) { 
                    var dragNote = seq[dragNoteIndex];
                    dragOffsetX = gridX - dragNote.x;
                    dragOffsetY = y - (musicToCanvasY(dragNote.y) - scrollOffset);
                }
            }
            else{
                selectedNoteIndex = -1
                selectedNoteIndices = []; // 清除多选
                dragNoteIndex = -1; // 清空拖动索引
                if(x >= keyWidth && x < canvas.width)
                {
                    var grid_space = all_note_width / grid
                    // 修复音符创建逻辑：先吸附画布Y坐标，再转换为音乐Y坐标
                    var canvasY_abs = y + scrollOffset;
                    var canvasY_snapped = snap(canvasY_abs, note_height);
                    var musicY_snapped = canvasToMusicY(canvasY_snapped);

                    seq.push({
                        "x":snap(gridX,grid_space),
                        "y":musicY_snapped, // 直接使用转换后的坐标
                        "width":grid_space,
                        "height":note_height
                    })
                    pattern[selPatternIndex].seq = [...seq]
                }
            }
            redraw();
        });
        canvas.addEventListener('mousemove', function(e) 
        {
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            var gridX = x - keyWidth
            
            // 更新框选区域
            if (isRectSelecting && isRectSelectMode) {
                rectSelectEndX = x;
                rectSelectEndY = y;
                
                // 更新选中的音符
                selectedNoteIndices = [];
                for (let i = 0; i < seq.length; i++) {
                    if (isNoteInRectSelection(i)) {
                        selectedNoteIndices.push(i);
                    }
                }
                
                redraw();
                return;
            }
            
            // 拖动/拉伸逻辑
            if ((selectedNoteIndex >= 0 || selectedNoteIndices.length > 0) && isMouseDown && dragNoteIndex >= 0)
            {
                var gridSpace = all_note_width / grid
                if(isStretching)
                {
                    stretchSelectedNotes(gridX, gridSpace); // 调用新的拉伸函数
                }
                else{
                    moveSelectedNotes(x,y,gridSpace)
                }
            }
        })
        canvas.addEventListener('mouseup', function(e) 
        {
            isMouseDown = false
            isStretching = false
            dragNoteIndex = -1; // 抬起鼠标时重置拖动/拉伸索引
            
            // 完成框选
            if (isRectSelecting) {
                isRectSelecting = false;
                
                // 如果有选中的音符，确保单选模式关闭
                if (selectedNoteIndices.length > 0) {
                    selectedNoteIndex = -1; 
                }
                
                redraw();
            }
        });
        function clamp(value,min,max)
        {
            return Math.min(Math.max(value,min),max)
        }
        scrollbarCanvas.addEventListener('mousedown', function(e) 
        {
            // ... (保持不变)
            const rect = scrollbarCanvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            scrollbarUiData.isMouseDown = true;
            // 计算滚动条比例
            const scrollbarRatio = scrollOffset / maxScrollOffset;
            const thumbPosition = scrollbarRatio * (scrollbarCanvas.height - scrollbarSliderHeight);
            if (y >= thumbPosition && y <= thumbPosition + scrollbarSliderHeight) 
            {
                // 记录点击位置相对于拇指顶部的偏移
                scrollbarUiData.dragOffset = y - thumbPosition;
            } else {
                // 点击在滚动条轨道上，直接跳转到该位置
                const newThumbPosition = Math.max(0, Math.min(scrollbarCanvas.height - scrollbarSliderHeight, y - scrollbarSliderHeight / 2));
                scrollbarUiData.y = newThumbPosition;
                scrollbarUiData.dragOffset = scrollbarSliderHeight / 2;
                
                // 更新 scrollOffset
                const ratio = newThumbPosition / (scrollbarCanvas.height - scrollbarSliderHeight);
                scrollOffset = ratio * maxScrollOffset;
            }
            redraw()
            redrawScrollbar()
        })
        scrollbarCanvas.addEventListener('mouseup', function(e) 
        {
            scrollbarUiData.isMouseDown = false
        })
        scrollbarCanvas.addEventListener('mousemove', function(e) 
        {
            const rect = scrollbarCanvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            if (scrollbarUiData.isMouseDown) {
                const rect = scrollbarCanvas.getBoundingClientRect();
                const y = e.clientY - rect.top;
                
                // 根据拖拽偏移计算新的拇指位置
                const newThumbPosition = Math.max(0, Math.min(scrollbarCanvas.height - scrollbarSliderHeight, y - scrollbarUiData.dragOffset));
                scrollbarUiData.y = newThumbPosition;
                
                // 更新 scrollOffset
                const ratio = newThumbPosition / (scrollbarCanvas.height - scrollbarSliderHeight);
                scrollOffset = ratio * maxScrollOffset;
                redraw()
                redrawScrollbar();
            }
        })
        scrollbarCanvas.addEventListener('mouseleave',function(e)
        {
            scrollbarUiData.isMouseDown = false
        })
        // 添加鼠标滚轮支持
        canvas.addEventListener('wheel', function(e) {
            e.preventDefault();
            const delta = e.deltaY > 0 ? 20 : -20;
            scrollOffset = Math.max(0, Math.min(maxScrollOffset, scrollOffset + delta));
            redraw();
        });
        function changeGrid(p_number)
        {
            // ... (保持不变)
            snapDanwei = p_number
            updateGridCount()
            if(selPatternIndex >=0 && pattern[selPatternIndex])
            {
                pattern[selPatternIndex].grid = grid
            }
            redrawGrid()
        }
        function updateGridCount()
        {
            // ... (保持不变)
            // 对三连音做特殊处理
            if(snapDanwei == 3 || snapDanwei == 6 ||snapDanwei == 12 || snapDanwei == 24 || snapDanwei == 48)
            {
                grid = paiziSettings.fenzi * snapDanwei
            }
            else
            {
                grid = paiziSettings.fenzi / paiziSettings.fenmu * snapDanwei
            }
            grid = Math.ceil(grid)
        }
        function changeStretch(checkbox)
        {
            isNoteStretch = checkbox.checked
            redraw() // 刷新以显示/隐藏拉伸手柄
        }
        function addPattern(self)
        {
            // ... (保持不变)
            pattern.push({
                "grid":16,
                "seq":[]
            })
            var btn = document.createElement('button')
            btn.innerHTML = pattern.length
            btn.onclick = function(){selPattern(btn)}
            //btn.oncontextmenu = function(){onPatternRightClick(btn)}
            PatternList.appendChild(btn)
        }
        function delSelPattern()
        {
            // ... (保持不变)
            seq = []
            pattern[selPatternIndex].seq = [];
            redraw()
        }
        function selPattern(self)
        {
            // ... (保持不变)
            for(var i=0;i<PatternList.children.length;i++)
            {
                PatternList.children[i].style = ""
            }
            self.style = "background-color: orange;"
            selPatternIndex = self.innerHTML -1
            if(pattern[selPatternIndex] && pattern[selPatternIndex].grid !=undefined )
            {
                grid = pattern[selPatternIndex].grid
            }
            if(pattern[selPatternIndex] && pattern[selPatternIndex].seq)
            {
                seq = [...pattern[selPatternIndex].seq];
            }
            else{
                seq = []
            }
            redraw()
        }
        // 把音符序序列的宽度转换为音符单位
        function convertSeqMusicStyle()
        {
            // ... (保持不变)
        }
        // 修改绘制函数，将音乐坐标转换为Canvas坐标
        function canvasToMusicY(canvasY)
        {
            // ... (保持不变)
            // 将Canvas坐标转换为音乐坐标（考虑滚动）
            //return (TOTAL_NOTES * note_height) - canvasY - note_height;
            return (TOTAL_NOTES * note_height) - canvasY - note_height // - scrollOffset;
        }
        function musicToCanvasY(musicY) 
        {
            // ... (保持不变)
            // musicY: 音乐坐标（低音=0，高音=大值）
            return (TOTAL_NOTES * note_height) - musicY - note_height;
        }
        function changeBeatFenzi(self)
        {
            // ... (保持不变)
            paiziSettings.fenzi = self.value
            updateGridCount()
            redrawGrid()
        }
        function changeBeatFenmu(self)
        {
            // ... (保持不变)
            paiziSettings.fenmu = self.value
            updateGridCount()
            redrawGrid()
        }
        function changeSelectMode(self)
        {
            // ... (保持不变)
            isRectSelectMode = self.checked
        }
        function copy()
        {
            // ... (保持不变)
            if(selPatternIndex >=0 && pattern[selPatternIndex])
            {
                copyedPatternData = {...pattern[selPatternIndex]}
            }
        }
        function paste()
        {
            // ... (保持不变)
            if(copyedPatternData)
            {
                pattern[selPatternIndex] = {...copyedPatternData}
                seq = [...pattern[selPatternIndex].seq]
                redraw()
            }
        }
        // 新增函数：处理选中音符的移动（单个或多个）
        function moveSelectedNotes(mouseX, mouseY, gridSpace) {
            // ... (保持不变)
            // 计算鼠标的相对移动量
            var gridX = mouseX - keyWidth;
            var adjustedX = gridX - dragOffsetX;
            // 修复音符移动逻辑：同样先吸附画布Y坐标，再转换
            var canvasY_top_abs = (mouseY - dragOffsetY) + scrollOffset;
            var canvasY_snapped = snap(canvasY_top_abs, note_height);
            var adjustedY_snapped = canvasToMusicY(canvasY_snapped);
            
            // 如果是单个音符选中 (或处于单选拖动状态)
            if (selectedNoteIndex >= 0 && selectedNoteIndices.length === 0) {
                moveSingleNote(selectedNoteIndex, adjustedX, adjustedY_snapped, gridSpace);
            } 
            // 如果是多个音符选中
            else if (selectedNoteIndices.length > 0) {
                moveMultipleNotes(adjustedX, adjustedY_snapped, gridSpace);
            }
            pattern[selPatternIndex].seq = [...seq];
            redraw();
        }
        // 移动单个音符
        function moveSingleNote(noteIndex, adjustedX, adjustedY, gridSpace) {
            // ... (保持不变)
            // 限制X坐标范围
            if (adjustedX < 0) adjustedX = 0;
            if (adjustedX + seq[noteIndex].width > canvas.width - keyWidth) {
                adjustedX = canvas.width - keyWidth - seq[noteIndex].width;
            }
            
            seq[noteIndex].x = snap(adjustedX, gridSpace);
            seq[noteIndex].y = adjustedY; // 直接使用转换后的坐标
        }

        // 移动多个音符
        function moveMultipleNotes(adjustedX, adjustedY, gridSpace) {
            // adjustedX/Y 是 *dragNote* 的目标坐标 (X未吸附, Y已吸附)
            if (dragNoteIndex < 0 || !seq[dragNoteIndex]) return; // 安全检查
            
            const dragNote = seq[dragNoteIndex];
            // 计算锚点音符的目标 X (吸附后)
            const snappedTargetX = snap(adjustedX, gridSpace);
            
            // 计算 *吸附后* 的增量 (delta)
            const deltaX = snappedTargetX - dragNote.x;
            const deltaY = adjustedY - dragNote.y; // adjustedY 已经吸附

            if (deltaX === 0 && deltaY === 0) return; // 没有移动

            // --- 边界检查 ---
            // 1. 找到所有选中音符的当前边界
            let minNoteX = Infinity;
            let maxNoteX = -Infinity;
            
            for (let i = 0; i < selectedNoteIndices.length; i++) {
                const note = seq[selectedNoteIndices[i]];
                if (note.x < minNoteX) minNoteX = note.x;
                if (note.x + note.width > maxNoteX) maxNoteX = note.x + note.width;
            }

            // 2. 计算受限的 deltaX
            let constrainedDeltaX = deltaX;
            const minCanvasX = 0; // x=0 (相对于音符区)
            const maxCanvasX = canvas.width - keyWidth;

            if (minNoteX + deltaX < minCanvasX) {
                // 撞到左墙
                constrainedDeltaX = minCanvasX - minNoteX;
            }
            if (maxNoteX + deltaX > maxCanvasX) {
                // 撞到右墙
                constrainedDeltaX = maxCanvasX - maxNoteX;
            }
            
            // 3. 应用受限的增量到所有选中的音符
            for (let i = 0; i < selectedNoteIndices.length; i++) {
                const noteIndex = selectedNoteIndices[i];
                const note = seq[noteIndex];
                
                note.x += constrainedDeltaX;
                note.y += deltaY; 
            }
        }

        // 【新增】拉伸选中的音符（单个或多个）
        function stretchSelectedNotes(gridX, gridSpace) {
            if (dragNoteIndex < 0) return; // 锚点音符不存在

            const anchorNote = seq[dragNoteIndex];
            
            // 1. 根据锚点音符计算新的宽度 (应用网格吸附)
            // gridX: 当前鼠标在音符区的 X 坐标
            let newWidth = gridX - anchorNote.x + dragOffsetX;
            
            // 最小宽度限制为 1 个 gridSpace
            newWidth = Math.max(gridSpace, newWidth); 

            // 应用网格吸附
            newWidth = Math.round(newWidth / gridSpace) * gridSpace;

            // 2. 边界检查: 确保锚点音符不会超出画布右侧边界
            const maxAllowedWidth = canvas.width - keyWidth - anchorNote.x;
            newWidth = Math.min(newWidth, maxAllowedWidth);
            
            // 3. 计算宽度变化量（delta）
            const widthDelta = newWidth - anchorNote.width;
            
            if (widthDelta === 0) return; // 没有变化

            // 4. 应用新的宽度到所有选中的音符
            let notesToStretch = [];
            if (selectedNoteIndex >= 0 && selectedNoteIndices.length === 0) {
                notesToStretch.push(selectedNoteIndex);
            } else if (selectedNoteIndices.length > 0) {
                notesToStretch = selectedNoteIndices;
            } else {
                return; // 没有音符被选中
            }
            
            for (let i = 0; i < notesToStretch.length; i++) {
                const noteIndex = notesToStretch[i];
                const note = seq[noteIndex];
                
                // 所有选中的音符应用相同的宽度变化量
                let finalNewWidth = note.width + widthDelta;
                
                // 再次进行边界检查（确保其他音符也不会超出）
                const maxIndividualAllowedWidth = canvas.width - keyWidth - note.x;
                finalNewWidth = Math.min(finalNewWidth, maxIndividualAllowedWidth);
                finalNewWidth = Math.max(gridSpace, finalNewWidth); // 最小宽度
                
                note.width = finalNewWidth;
            }
            
            pattern[selPatternIndex].seq = [...seq];
            redraw();
        }
    </script>
</body>
</html>